//*****************************************************************************
// JFab (http://code.google.com/p/jfab)
// Copyright (c) 2011-2012 JFab.org
// Admin jfab@jeffreythompson.net
//
// See the file "LICENSE.txt" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//*****************************************************************************

package org.jfab.swingui;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Method;

import javax.swing.ButtonGroup;
import javax.swing.Icon;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JSplitPane;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.UIManager;
import javax.swing.WindowConstants;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.jfab.core.collection.CGClassList;
import org.jfab.core.collection.CGEnumList;
import org.jfab.core.collection.CGInterfaceList;
import org.jfab.core.inflator.CGSpecInflator;
import org.jfab.core.inflator.DefaultInflatorInjector;
import org.jfab.core.inflator.InflatorInjector;
import org.jfab.core.io.CGSpecReader;
import org.jfab.core.io.CGSpecWriter;
import org.jfab.core.io.IOReaderException;
import org.jfab.core.io.IOWriterException;
import org.jfab.core.model.CGClass;
import org.jfab.core.model.CGCompilationUnit;
import org.jfab.core.model.CGEnum;
import org.jfab.core.model.CGInterface;
import org.jfab.core.model.CGObject;
import org.jfab.core.model.CGSpec;
import org.jfab.swingui.decorator.Decorator;
import org.jfab.swingui.util.OSXApp;
import org.jfab.swingui.util.RealizedThread;
import org.jfab.swingui.util.SystemUtilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Provides a user interface for JFab.
 * <p>
 * for run configuration: -Xdock:name="JFab UI"
 * </p>
 * <p>
 * application argument: -Dswing.defaultlaf=com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel (but disallows screen menu
 * bar)
 * </p>
 */
public class JFabUI extends JPanel implements ActionPerformer, OSXApp
{
    /** Logger. */
    private static final Logger LOGGER = LoggerFactory.getLogger(JFabUI.class);

    /** Serial version UID. */
    private static final long serialVersionUID = 1L;

    /** Frame. */
    static JFrame _frame;

    /**
     * @return the frame
     */
    public static JFrame getFrame()
    {
        return _frame;
    }

    /**
     * Application method.
     * 
     * @param args Application arguments.
     */
    public static final void main(final String[] args)
    {
        System.setProperty("apple.laf.useScreenMenuBar", "true");
        System.setProperty("com.apple.mrj.application.apple.menu.about.name", "JFab UI");
        try
        {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        }
        catch (final Exception e)
        {
            System.err.println("Unable to set look and feel: " + UIManager.getSystemLookAndFeelClassName());
        }

        EventQueue.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                final JFabUI panel = new JFabUI();

                _frame = new JFrame("JFab UI");
                _frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
                _frame.getContentPane().add(panel, BorderLayout.CENTER);
                _frame.setSize(1280, 1024 - 160);
                _frame.setLocationByPlatform(true);
                _frame.setJMenuBar(panel.getMenuBar());
                _frame.setVisible(true);
            }
        });
    }

    /** Code panel. */
    final CodePanel _codePanel = new CodePanel();

    /** Edit panel. */
    final EditPanel _editPanel;

    /** File chooser. */
    JFileChooser _fileChooser;

    /** Action manager. */
    final ActionManager _actionManager;

    /** Code generator specification. */
    private CGSpec _cgSpec;

    /** Inflated code generator specification. */
    private CGSpec _inflatedCgSpec;

    /** File. */
    private File _file;

    /** Icon manager. */
    private final IconManager _iconManager = new IconManager();

    /** Flag indicating if the code generator specification is changed. */
    private boolean _isDirty = false;

    /** Main panel. */
    private final JSplitPane _mainPanel;

    /** Menu bar. */
    private final JMenuBar _menuBar;

    /** Tool bar panel. */
    private final JPanel _toolBarPanel;

    /** Tree widget. */
    private final TreePanel _treePanel;

    /**
     * Construct this object.
     */
    public JFabUI()
    {
        // Set up our application to respond to the Mac OS X application menu
        macOSXRegistration();

        _actionManager = new ActionManager(this, _iconManager);

        setLayout(new BorderLayout());

        _cgSpec = new CGSpec();
        _treePanel = createTreeUI(_cgSpec);
        _editPanel = createEditPanel();
        _mainPanel = createMainPanel();

        _menuBar = createMenuBar();

        _toolBarPanel = createToolBarPanel();
        _toolBarPanel.add(_mainPanel, BorderLayout.CENTER);

        add(_toolBarPanel, BorderLayout.CENTER);

        checkActions();
    }

    @Override
    public void aboutActionPerformed(final ActionEvent event)
    {
        final String description = "JFab is a code generator tool.\n\nFor more information or to contribute, please see the open source\nproject site at http://code.google.com/p/jfab/"
                + "\n\nCopyright \u00A9 2012 JFab.org. All rights reserved.\n\n";

        final String title = "About JFabUI";
        final Icon icon = _iconManager.getAboutMessageIcon();

        JOptionPane.showMessageDialog(getFrame(), description, title, JOptionPane.INFORMATION_MESSAGE, icon);
    }

    @Override
    public void addNodeActionPerformed(final ActionEvent event)
    {
        final Decorator<?> decorator = _treePanel.getSelectedObject();

        final JPanel message = new JPanel(new GridLayout(0, 1));
        message.add(new JLabel("Select a node type:"));
        final ButtonGroup buttonGroup = new ButtonGroup();
        final String title = "Add Node";
        final int messageType = JOptionPane.QUESTION_MESSAGE;
        final String[] selectionValues = decorator.getChildSelectionValues();

        for (int i = 0; i < selectionValues.length; i++)
        {
            final JRadioButton button = new JRadioButton(selectionValues[i]);
            button.setActionCommand(selectionValues[i]);
            buttonGroup.add(button);
            message.add(button);
            if (i == 0)
            {
                button.setSelected(true);
            }
        }

        JOptionPane.showMessageDialog(getFrame(), message, title, messageType);
        final String response = buttonGroup.getSelection().getActionCommand();

        if (StringUtils.isNotEmpty(response))
        {
            decorator.addNode(response);
            _treePanel.setCgSpec(_cgSpec);
        }

        checkActions();
    }

    @Override
    public void collapseAllActionPerformed(final ActionEvent event)
    {
        _treePanel.collapseAll();
    }

    @Override
    public void expandAllActionPerformed(final ActionEvent event)
    {
        _treePanel.expandAll();
    }

    @Override
    public void generateActionPerformed(final ActionEvent event)
    {
        System.out.println("generateActionPerformed()");

        checkActions();
    }

    @Override
    public void newActionPerformed(final ActionEvent event)
    {
        if (isDirty())
        {
            // FIXME: Prompt to save first.
        }

        setCgSpec(new CGSpec());

        setDirty(false);

        checkActions();
    }

    @Override
    public void openActionPerformed(final ActionEvent event)
    {
        if (isDirty())
        {
            // FIXME: Prompt to save first.
        }

        final int returnVal = getFileChooser().showOpenDialog(getFrame());

        if (returnVal == JFileChooser.APPROVE_OPTION)
        {
            _file = getFileChooser().getSelectedFile();
            final CGSpec cgSpec = readCgSpec(_file);
            setCgSpec(cgSpec);
            setTitle(_file.getName());
            setDirty(false);
        }

        checkActions();
    }

    @Override
    public void preferencesActionPerformed(final ActionEvent event)
    {
        // Nothing to do.
    }

    @Override
    public void quitActionPerformed(final ActionEvent event)
    {
        System.exit(0);
    }

    @Override
    public void removeNodeActionPerformed(final ActionEvent event)
    {
        System.out.println("removeNodeActionPerformed()");

        // FIXME: prompt Are you sure?

        _treePanel.removeSelectedNode();

        checkActions();
    }

    @Override
    public void saveActionPerformed(final ActionEvent event)
    {
        if (_file == null)
        {
            saveAsActionPerformed(event);
        }

        if (_file != null)
        {
            saveCgSpec(getCgSpec(), _file);

            setDirty(false);
        }

        checkActions();
    }

    @Override
    public void saveAsActionPerformed(final ActionEvent event)
    {
        final int returnVal = getFileChooser().showSaveDialog(getFrame());

        if (returnVal == JFileChooser.APPROVE_OPTION)
        {
            _file = getFileChooser().getSelectedFile();
            saveCgSpec(getCgSpec(), _file);
            setTitle(_file.getName());
            setDirty(false);
        }

        checkActions();
    }

    @Override
    public void saveCgObjectActionPerformed(final ActionEvent event)
    {
        if (_editPanel.isDirty())
        {
            _editPanel.applyChanges();
            setCgSpec(_cgSpec);
        }

        checkActions();
    }

    /**
     * Check the enabled state of actions.
     */
    void checkActions()
    {
        _actionManager.getSaveAction().setEnabled(isDirty());

        final Decorator<?> decorator = _treePanel.getSelectedObject();
        final boolean isSelected = decorator != null;
        final boolean hasChildren = (decorator == null ? false : ArrayUtils.isNotEmpty(decorator
                .getChildSelectionValues()));
        final boolean isNotRoot = decorator != _treePanel.getRootNode();
        _actionManager.getAddNodeAction().setEnabled(isSelected && hasChildren);
        _actionManager.getRemoveNodeAction().setEnabled(isSelected && isNotRoot);
    }

    /**
     * @param decorator Decorator.
     * 
     * @return the code generator compilation unit which corresponds to the given parameter.
     */
    CGCompilationUnit findInInflatedCgSpec(final Decorator<?> decorator)
    {
        CGCompilationUnit answer = null;

        final CGCompilationUnit cgCompilationUnit = findInCgSpec(decorator);

        if (cgCompilationUnit != null)
        {
            if (cgCompilationUnit instanceof CGInterface)
            {
                final CGInterfaceList list = new CGInterfaceList(_inflatedCgSpec.getInterfaces());
                answer = list.findByName(cgCompilationUnit.getName());
            }
            else if (cgCompilationUnit instanceof CGClass)
            {
                final CGClassList list = new CGClassList(_inflatedCgSpec.getClasses());
                answer = list.findByName(cgCompilationUnit.getName());
            }
            else if (cgCompilationUnit instanceof CGEnum)
            {
                final CGEnumList list = new CGEnumList(_inflatedCgSpec.getEnums());
                answer = list.findByName(cgCompilationUnit.getName());
            }
        }

        return answer;
    }

    /**
     * @return the menuBar
     */
    JMenuBar getMenuBar()
    {
        return _menuBar;
    }

    /**
     * @param isDirty the isDirty to set
     */
    void setDirty(final boolean isDirty)
    {
        _isDirty = isDirty;
    }

    /**
     * @return a new edit panel.
     */
    private EditPanel createEditPanel()
    {
        final EditPanel answer = new EditPanel(_actionManager);

        return answer;
    }

    /**
     * @return a new main panel.
     */
    private JSplitPane createMainPanel()
    {
        final JSplitPane splitPane = new JSplitPane();

        splitPane.setOneTouchExpandable(true);
        splitPane.setTopComponent(_editPanel);
        splitPane.setBottomComponent(_codePanel);
        new RealizedThread(splitPane)
        {
            @Override
            public void realized()
            {
                ((JSplitPane)_component).setDividerLocation(0.5);
            }
        }.start();

        final JSplitPane answer = new JSplitPane();

        answer.setLeftComponent(_treePanel);
        answer.setRightComponent(splitPane);

        return answer;
    }

    /**
     * @return a new menu bar.
     */
    private JMenuBar createMenuBar()
    {
        final JMenu fileMenu = new JMenu("File");

        fileMenu.add(_actionManager.getNewAction());
        fileMenu.add(_actionManager.getOpenAction());
        fileMenu.add(_actionManager.getSaveAction());
        fileMenu.add(_actionManager.getSaveAsAction());

        final JMenuBar answer = new JMenuBar();

        answer.add(fileMenu);

        return answer;
    }

    /**
     * @return a new tool bar.
     */
    private JToolBar createToolBar()
    {
        final JToolBar answer = new JToolBar("JFab Tool Bar");

        answer.add(_actionManager.getNewAction());
        answer.add(_actionManager.getOpenAction());
        answer.add(_actionManager.getSaveAction());
        answer.addSeparator();
        answer.add(_actionManager.getGenerateAction());
        answer.addSeparator();
        answer.add(_actionManager.getAboutAction());

        return answer;
    }

    /**
     * @return a new tool bar panel.
     */
    private JPanel createToolBarPanel()
    {
        final JPanel answer = new JPanel(new BorderLayout());

        answer.add(createToolBar(), BorderLayout.PAGE_START);

        return answer;
    }

    /**
     * @param cgSpec Code generator specification.
     * 
     * @return a new tree widget.
     */
    private TreePanel createTreeUI(final CGSpec cgSpec)
    {
        final TreePanel answer = new TreePanel(cgSpec, _actionManager, _iconManager);

        // Listen for when the selection changes.
        answer.addTreeSelectionListener(new TreeSelectionListener()
        {
            @Override
            public void valueChanged(final TreeSelectionEvent event)
            {
                if (_editPanel.isDirty())
                {
                    final Object message = "Do you want to save changes?";
                    final int response = JOptionPane.showConfirmDialog(getFrame(), message, "Confirm Save",
                            JOptionPane.YES_NO_OPTION);

                    if (response == JOptionPane.YES_OPTION)
                    {
                        _editPanel.applyChanges();
                    }
                }

                final JTree tree = (JTree)event.getSource();
                final Object object = tree.getLastSelectedPathComponent();

                if (object instanceof Decorator<?>)
                {
                    final Decorator<?> decorator = (Decorator<?>)object;

                    _editPanel.setDecorator(decorator);
                    final CGCompilationUnit cgCompilationUnit = findInInflatedCgSpec(decorator);
                    _codePanel.setCgCompilationUnit(cgCompilationUnit);
                    setDirty(true);
                }

                checkActions();
            }
        });

        return answer;
    }

    /**
     * @param decorator Decorator.
     * 
     * @return the code generator compilation unit which corresponds to the given parameter.
     */
    private CGCompilationUnit findInCgSpec(final Decorator<?> decorator)
    {
        CGCompilationUnit answer = null;

        if ((decorator != null) && (decorator.getComponent() instanceof CGObject))
        {
            final CGObject cgObject = (CGObject)decorator.getComponent();

            if (cgObject instanceof CGCompilationUnit)
            {
                answer = (CGCompilationUnit)cgObject;
            }
            else if (cgObject != null)
            {
                // Walk up the tree until a compilation unit is found.
                Decorator<?> parent = decorator.getParent();
                CGObject myCgObject = cgObject;

                while ((parent != null) && (myCgObject != null) && !(myCgObject instanceof CGCompilationUnit))
                {
                    if (parent.getComponent() instanceof CGObject)
                    {
                        myCgObject = (CGObject)parent.getComponent();
                    }

                    parent = parent.getParent();
                }

                answer = (CGCompilationUnit)myCgObject;
            }
        }

        return answer;
    }

    /**
     * @return the cgSpec
     */
    private CGSpec getCgSpec()
    {
        return _cgSpec;
    }

    /**
     * @return the fileChooser
     */
    private JFileChooser getFileChooser()
    {
        if (_fileChooser == null)
        {
            _fileChooser = new JFileChooser();
        }

        return _fileChooser;
    }

    /**
     * @param cgSpec Code generator specification.
     * 
     * @return an inflated code generator specification.
     */
    private CGSpec inflateCgSpec(final CGSpec cgSpec)
    {
        final InflatorInjector injector = new DefaultInflatorInjector();
        final CGSpecInflator inflator = injector.injectCGSpecInflator();

        CGSpec answer = null;

        if (cgSpec != null)
        {
            answer = inflator.inflate(cgSpec);
        }

        return answer;
    }

    /**
     * @return the isDirty
     */
    private boolean isDirty()
    {
        return _isDirty;
    }

    /**
     * Generic registration with the Mac OS X application menu. Checks the platform, then attempts to register with the
     * Apple EAWT. This method calls OSXAdapter.registerMacOSXApplication() and OSXAdapter.enablePrefs(). See
     * OSXAdapter.java for the signatures of these methods.
     */
    private void macOSXRegistration()
    {
        final SystemUtilities systemUtils = new SystemUtilities();

        if (systemUtils.isMacPlatform() && !systemUtils.isApplet())
        {
            try
            {
                final Class<?> osxAdapter = Class.forName("org.jfab.swingui.util.OSXAdapter");

                final Class<?>[] defArgs = { OSXApp.class };
                final Method registerMethod = osxAdapter.getDeclaredMethod("registerMacOSXApplication", defArgs);

                if (registerMethod != null)
                {
                    final Object[] args = { this };
                    registerMethod.invoke(osxAdapter, args);
                }
            }
            catch (final NoClassDefFoundError e)
            {
                /*
                 * This will be thrown first if the OSXAdapter is loaded on a system without the EAWT because OSXAdapter
                 * extends ApplicationAdapter in its def
                 */
                LOGGER.error(
                        "This version of Mac OS X does not support the Apple EAWT.  Application Menu handling has been disabled.",
                        e);
            }
            catch (final ClassNotFoundException e)
            {
                /*
                 * This shouldn't be reached; if there's a problem with the OSXAdapter we should get the above
                 * NoClassDefFoundError first.
                 */
                LOGGER.error(
                        "This version of Mac OS X does not support the Apple EAWT.  Application Menu handling has been disabled.",
                        e);
            }
            catch (final Exception e)
            {
                LOGGER.error(e.getMessage(), e);
            }
        }
    }

    /**
     * @param file File.
     * 
     * @return a code generator specification from the given parameter.
     */
    private CGSpec readCgSpec(final File file)
    {
        CGSpec answer = null;

        Reader reader = null;

        try
        {
            reader = new FileReader(file);

            final CGSpecReader jfabReader = new CGSpecReader();
            answer = jfabReader.read(reader);
        }
        catch (final FileNotFoundException e)
        {
            throw new IOReaderException(e);
        }
        catch (final Exception e)
        {
            throw new IOReaderException(e);
        }
        finally
        {
            if (reader != null)
            {
                try
                {
                    reader.close();
                }
                catch (final IOException ignore)
                {
                    // Nothing to do.
                }
            }
        }

        return answer;
    }

    /**
     * @param cgSpec Code generator specification.
     * @param file File.
     */
    private void saveCgSpec(final CGSpec cgSpec, final File file)
    {
        Writer writer = null;

        try
        {
            writer = new FileWriter(file);
            final CGSpecWriter jfabWriter = new CGSpecWriter();
            jfabWriter.write(writer, cgSpec);
        }
        catch (final IOException e)
        {
            throw new IOWriterException(e);
        }
        finally
        {
            if (writer != null)
            {
                try
                {
                    writer.flush();
                    writer.close();
                }
                catch (final IOException ignore)
                {
                    // Nothing to do.
                }
            }
        }
    }

    /**
     * @param cgSpec the cgSpec to set
     */
    private void setCgSpec(final CGSpec cgSpec)
    {
        _cgSpec = cgSpec;
        _inflatedCgSpec = inflateCgSpec(cgSpec);

        _treePanel.setCgSpec(cgSpec);
        _editPanel.setDecorator(null);
        _codePanel.setCgCompilationUnit(null);
    }

    /**
     * @param title Title.
     */
    private void setTitle(final String title)
    {
        getFrame().setTitle("JFab UI: " + title);
    }
}
